<template>
  <teleport to="body" :disabled="!teleported">
    <transition name="viewer-fade" appear>
      <div ref="wrapper" :tabindex="-1" class="el-image-viewer__wrapper" :style="{ zIndex: computedZIndex }">
        <div class="el-image-viewer__mask" @click.self="hideOnClickModal && hide()" />
        <!-- CLOSE -->
        <span class="el-image-viewer__btn el-image-viewer__close" @click="hide">
          <el-icon><Close /></el-icon>
        </span>

        <!-- ARROW -->
        <template v-if="!isSingle">
          <span
            :class="[
              'el-image-viewer__btn',
              'el-image-viewer__prev',
              { 'el-image-viewer__disabled': !infinite && isFirst }
            ]"
            @click="prev"
          >
            <el-icon><ArrowLeft /></el-icon>
          </span>
          <span
            :class="[
              'el-image-viewer__btn',
              'el-image-viewer__next',
              { 'el-image-viewer__disabled': !infinite && isLast }
            ]"
            @click="next"
          >
            <el-icon><ArrowRight /></el-icon>
          </span>
        </template>
        <!-- ACTIONS -->
        <div class="el-image-viewer__btn el-image-viewer__actions">
          <div class="el-image-viewer__actions__inner">
            <el-icon @click="handleActions('zoomOut')">
              <ZoomOut />
            </el-icon>
            <el-icon @click="handleActions('zoomIn')">
              <ZoomIn />
            </el-icon>
            <i class="el-image-viewer__actions__divider" />
            <el-icon @click="toggleMode">
              <FullScreen v-show="mode.name === 'contain'" />
              <ScaleToOriginal v-show="mode.name === 'original'" />
            </el-icon>
            <el-icon @click="handleActions('download')">
              <Download />
            </el-icon>
            <i class="el-image-viewer__actions__divider" />
            <el-icon @click="handleActions('anticlockwise')">
              <RefreshLeft />
            </el-icon>
            <el-icon @click="handleActions('clockwise')">
              <RefreshRight />
            </el-icon>
          </div>
        </div>
        <!-- CANVAS -->
        <div class="el-image-viewer__canvas">
          <img
            v-for="(url, i) in urlList"
            v-show="i === activeIndex"
            :ref="(el) => (imgRefs[i] = el)"
            :key="url"
            :src="url"
            :style="imgStyle"
            class="el-image-viewer__img"
            @load="handleImgLoad"
            @error="handleImgError"
            @mousedown="handleMouseDown"
          ></img>
        </div>
      </div>
    </transition>
  </teleport>
</template>

<script setup>
import { ref, shallowRef, computed, effectScope, watch, nextTick, onMounted } from 'vue'
import { throttle } from 'lodash-unified'
import { useEventListener } from '@vueuse/core'
import { downloadByBlob } from '../../utils'

const props = defineProps({
  /** 用于预览的图片链接列表 */
  urlList: {
    type: Array,
    default: () => []
  },
  /** 预览时遮罩层的 z-index */
  zIndex: {
    type: Number,
    default: null
  },
  /** 初始预览图像索引，小于 url-list 的长度 */
  initialIndex: {
    type: Number,
    default: 0
  },
  /** 是否可以无限循环预览 */
  infinite: {
    type: Boolean,
    default: true
  },
  /** 是否可以通过点击遮罩层关闭 */
  hideOnClickModal: {
    type: Boolean,
    default: false
  },
  /** image-viewer 是否插入至 body 元素上 */
  teleported: {
    type: Boolean,
    default: false
  },
  /** 是否可以通过按下 ESC 关闭 Image Viewer */
  closeOnPressEscape: {
    type: Boolean,
    default: true
  },
  zoomRate: {
    type: Number,
    default: 1.2
  }
})

const emit = defineEmits(['close', 'switch'])

const scopeEventListener = effectScope()

const isSingle = computed(() => {
  const { urlList } = props
  return urlList.length <= 1
})

const modes = {
  CONTAIN: {
    name: 'contain'
  },
  ORIGINAL: {
    name: 'original'
  }
}

const EVENT_CODE = {
  tab: 'Tab',
  enter: 'Enter',
  space: 'Space',
  left: 'ArrowLeft', // 37
  up: 'ArrowUp', // 38
  right: 'ArrowRight', // 39
  down: 'ArrowDown', // 40
  esc: 'Escape',
  delete: 'Delete',
  backspace: 'Backspace',
  numpadEnter: 'NumpadEnter',
  pageUp: 'PageUp',
  pageDown: 'PageDown',
  home: 'Home',
  end: 'End'
}

const wrapper = ref()
const imgRefs = ref([])

const loading = ref(true)
const activeIndex = ref(props.initialIndex)
const mode = shallowRef(modes.CONTAIN)
const transform = ref({
  scale: 1,
  deg: 0,
  offsetX: 0,
  offsetY: 0,
  enableTransition: false
})

const isFirst = computed(() => {
  return activeIndex.value === 0
})

const isLast = computed(() => {
  return activeIndex.value === props.urlList.length - 1
})

const currentImg = computed(() => {
  return props.urlList[activeIndex.value]
})

const imgStyle = computed(() => {
  const { scale, deg, offsetX, offsetY, enableTransition } = transform.value
  let translateX = offsetX / scale
  let translateY = offsetY / scale

  switch (deg % 360) {
    case 90:
    case -270:
      ;[translateX, translateY] = [translateY, -translateX]
      break
    case 180:
    case -180:
      ;[translateX, translateY] = [-translateX, -translateY]
      break
    case 270:
    case -90:
      ;[translateX, translateY] = [-translateY, translateX]
      break
  }

  const style = {
    transform: `scale(${scale}) rotate(${deg}deg) translate(${translateX}px, ${translateY}px)`,
    transition: enableTransition ? 'transform .3s' : ''
  }
  if (mode.value.name === modes.CONTAIN.name) {
    style.maxWidth = style.maxHeight = '100%'
  }
  return style
})

const computedZIndex = computed(() => {
  return typeof props.zIndex === 'number' ? props.zIndex : 9999
})

function hide() {
  unregisterEventListener()
  emit('close')
}

function registerEventListener() {
  const keydownHandler = throttle((e) => {
    switch (e.code) {
      // ESC
      case EVENT_CODE.esc:
        props.closeOnPressEscape && hide()
        break
      // SPACE
      case EVENT_CODE.space:
        toggleMode()
        break
      // LEFT_ARROW
      case EVENT_CODE.left:
        prev()
        break
      // UP_ARROW
      case EVENT_CODE.up:
        handleActions('zoomIn')
        break
      // RIGHT_ARROW
      case EVENT_CODE.right:
        next()
        break
      // DOWN_ARROW
      case EVENT_CODE.down:
        handleActions('zoomOut')
        break
    }
  })
  const mousewheelHandler = throttle((e) => {
    const delta = e.deltaY || e.deltaX
    handleActions(delta < 0 ? 'zoomIn' : 'zoomOut', {
      zoomRate: props.zoomRate,
      enableTransition: false
    })
  })

  scopeEventListener.run(() => {
    useEventListener(document, 'keydown', keydownHandler)
    useEventListener(document, 'wheel', mousewheelHandler)
  })
}

function unregisterEventListener() {
  scopeEventListener.stop()
}

function handleImgLoad() {
  loading.value = false
}

function handleImgError(e) {
  loading.value = false
  e.target.alt = '图片加载失败..'
}

function handleMouseDown(e) {
  if (loading.value || e.button !== 0 || !wrapper.value) return
  transform.value.enableTransition = false

  const { offsetX, offsetY } = transform.value
  const startX = e.pageX
  const startY = e.pageY

  const dragHandler = throttle((ev) => {
    transform.value = {
      ...transform.value,
      offsetX: offsetX + ev.pageX - startX,
      offsetY: offsetY + ev.pageY - startY
    }
  })
  const removeMousemove = useEventListener(document, 'mousemove', dragHandler)
  useEventListener(document, 'mouseup', () => {
    removeMousemove()
  })

  e.preventDefault()
}

function reset() {
  transform.value = {
    scale: 1,
    deg: 0,
    offsetX: 0,
    offsetY: 0,
    enableTransition: false
  }
}

function toggleMode() {
  if (loading.value) return

  const modeNames = Object.keys(modes)
  const modeValues = Object.values(modes)
  const currentMode = mode.value.name
  const index = modeValues.findIndex((i) => i.name === currentMode)
  const nextIndex = (index + 1) % modeNames.length
  mode.value = modes[modeNames[nextIndex]]
  reset()
}

function setActiveItem(index) {
  const len = props.urlList.length
  activeIndex.value = (index + len) % len
}

function prev() {
  if (isFirst.value && !props.infinite) return
  setActiveItem(activeIndex.value - 1)
}

function next() {
  if (isLast.value && !props.infinite) return
  setActiveItem(activeIndex.value + 1)
}

function handleActions(action, options = {}) {
  if (loading.value) return
  const { zoomRate, rotateDeg, enableTransition } = {
    zoomRate: props.zoomRate,
    rotateDeg: 90,
    enableTransition: true,
    ...options
  }
  switch (action) {
    case 'zoomOut':
      if (transform.value.scale > 0.2) {
        transform.value.scale = Number.parseFloat((transform.value.scale / zoomRate).toFixed(3))
      }
      break
    case 'zoomIn':
      if (transform.value.scale < 7) {
        transform.value.scale = Number.parseFloat((transform.value.scale * zoomRate).toFixed(3))
      }
      break
    case 'clockwise':
      transform.value.deg += rotateDeg
      break
    case 'anticlockwise':
      transform.value.deg -= rotateDeg
      break
    case 'download':
      downloadByBlob(currentImg.value, 'download')
      break
  }
  transform.value.enableTransition = enableTransition
}

watch(currentImg, () => {
  nextTick(() => {
    const $img = imgRefs.value[0]
    if (!$img?.complete) {
      loading.value = true
    }
  })
})

watch(activeIndex, (val) => {
  reset()
  emit('switch', val)
})

onMounted(() => {
  registerEventListener()
  // add tabindex then wrapper can be focusable via Javascript
  // focus wrapper so arrow key can't cause inner scroll behavior underneath
  wrapper.value?.focus?.()
})
</script>

<style lang="scss" scoped>
.el-image-viewer__disabled {
  display: none;
}
</style>
